/*
*Copyright 2021-2023 NERCIS
*
*Licensed under the Apache License, Version 2.0 (the "License");
*you may not use this file except in compliance with the License.
*You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
*Unless required by applicable law or agreed to in writing, software
*distributed under the License is distributed on an "AS IS" BASIS,
*WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
*See the License for the specific language governing permissions and
*limitations under the License.
*/

package cn.ac.nercis.pes.community.controller.evaluation.photo;

import cn.ac.nercis.pes.common.constant.knowledge.LayerTarget;
import cn.ac.nercis.pes.common.utils.ConvertUtils;
import cn.ac.nercis.pes.community.component.controls.MenuListCell;
import cn.ac.nercis.pes.community.component.controls.PesSwitch;
import cn.ac.nercis.pes.community.component.dialog.*;
import cn.ac.nercis.pes.community.component.photo.PhotoSelectControl;
import cn.ac.nercis.pes.community.constant.AppConstants;
import cn.ac.nercis.pes.community.model.vo.MenuVO;
import cn.ac.nercis.pes.community.utils.*;
import cn.ac.nercis.pes.community.service.photo.PhotoFileService;
import cn.ac.nercis.pes.service.utils.FileManage;
import javafx.animation.KeyFrame;
import javafx.animation.Timeline;
import javafx.application.Platform;
import javafx.embed.swing.SwingFXUtils;
import javafx.fxml.FXML;
import javafx.fxml.Initializable;
import javafx.scene.control.Button;
import javafx.scene.control.Label;
import javafx.scene.control.ListView;
import javafx.scene.control.TextField;
import javafx.scene.image.Image;
import javafx.scene.image.ImageView;
import javafx.scene.input.*;
import javafx.scene.layout.AnchorPane;
import javafx.scene.layout.FlowPane;
import javafx.scene.layout.StackPane;
import javafx.util.Duration;
import lombok.NonNull;
import lombok.extern.slf4j.Slf4j;
import net.coobird.thumbnailator.Thumbnails;
import org.apache.commons.collections4.CollectionUtils;
import org.apache.commons.io.FileUtils;
import org.apache.commons.lang3.StringUtils;
import org.springframework.data.domain.PageRequest;

import javax.imageio.ImageIO;
import java.io.File;
import java.io.IOException;
import java.net.URL;
import java.time.LocalDateTime;
import java.util.List;
import java.util.Objects;
import java.util.ResourceBundle;
import java.util.concurrent.CompletableFuture;
import java.util.stream.Collectors;
import java.util.stream.IntStream;

/**
 * 证据照片选择控制器
 * @author zpy
 * @version 1.0.0
 */
@Slf4j
public class PhotoSelectController implements Initializable {
    private static final Integer BLOCK_LIMIT_COUNT = 20;
    private static final String TITLE_FORMAT = "%s[证据文件目录]";
    @FXML
    private StackPane page;
    @FXML
    private Label titleLbl;
    @FXML
    private ListView<MenuVO> menuListView;
    @FXML
    private TextField searchTxt;
    @FXML
    private Button searchButton;
    @FXML
    private PesSwitch compressSwitch;
    @FXML
    private FlowPane photoFlow;
    @FXML
    private AnchorPane emptyPane;
    @FXML
    private ImageView emptyImageView;
    @FXML
    private Label emptyLabel;

    private final PhotoFileService photoFileService;

    private LayerTarget currentLayerTarget;
    private Timeline timeline = null;
    private String currentSystemId;

    public PhotoSelectController() {
        this.photoFileService = SpringUtils.getBean(PhotoFileService.class);
    }

    @Override
    public void initialize(URL location, ResourceBundle resources) {
        searchButton.setGraphic(IconUtils.getGlyph("search-icon","btn-icon"));
        initializeMenu();
        ControlUtils.initEmptyImageView(this.emptyImageView,log);
        page.setOnKeyPressed(this::onKeyPressed);
    }

    private void onKeyPressed(KeyEvent keyEvent){
        if(keyEvent.isShortcutDown() && keyEvent.getCode() == KeyCode.V){
            var clipboard= Clipboard.getSystemClipboard();
            if(clipboard.hasContent(DataFormat.FILES)){
                this.appendSelectFiles(clipboard.getFiles());
                this.setRenderEmpty(false);
            }else if(clipboard.hasContent(DataFormat.IMAGE)){
                var screenshotFile = generateScreenshotFile(clipboard.getImage());
                if(Objects.nonNull(screenshotFile)){
                    this.addPhotoImage(screenshotFile);
                    this.setRenderEmpty(false);
                }
            }
        }
    }

    private File generateScreenshotFile(Image image){
        if(Objects.isNull(image)){
            return null;
        }
        var fileDir = FileUtils.getFile(FileManage.getPhotoFileDir(this.currentSystemId,currentLayerTarget.getName()));
        File file= null;
        try {
            FileManage.createDirectory(fileDir);
            if(compressSwitch.isSelected()){
                file = FileUtils.getFile(fileDir,String.format("%s%s.jpg",currentLayerTarget.getName(),
                        ConvertUtils.toFileNameString(LocalDateTime.now())));
                Thumbnails.of(SwingFXUtils.fromFXImage(image,null))
                        .scale(1f)
                        .outputQuality(AppConstants.IMAGE_COMPRESS)
                        .toFile(file);
            }else{
                file = FileUtils.getFile(fileDir,String.format("%s%s.png",currentLayerTarget.getName(),
                        ConvertUtils.toFileNameString(LocalDateTime.now())));
                ImageIO.write(SwingFXUtils.fromFXImage(image,null), AppConstants.SCREENSHOT_DEFAULT_FORMAT,file);
            }
        }catch (Exception ex){
            log.error("【选择证据文件】生成截图文件出错：",ex);
        }
        return file;
    }

    public void initializeData(String systemId,LayerTarget layerTarget){
        LogUtils.info(log, "初始化证据照片选择...");
        this.currentSystemId = systemId;
        this.searchTxt.setText("");
        if(Objects.isNull(layerTarget)){
            onQueryClick();
        }else if(currentLayerTarget != layerTarget){
            menuListView.getItems()
                    .stream()
                    .filter(item-> StringUtils.equals(item.getTitle(),layerTarget.getName()))
                    .findFirst()
                    .ifPresent(item->menuListView.getSelectionModel().select(item));
        }else{
            loadContent();
        }
    }

    public List<File> selectedPhotoFile(){
        return photoFlow.getChildren()
                .stream()
                .filter(node -> node instanceof PhotoSelectControl)
                .map(node -> (PhotoSelectControl) node)
                .filter(PhotoSelectControl::getSelected)
                .map(PhotoSelectControl::getPictureFile)
                .collect(Collectors.toList());
    }

    @FXML
    private void onQueryClick() {
        currentLayerTarget = null;
        this.menuListView.getSelectionModel().clearSelection();
        String queryText = this.searchTxt.getText();
        LogUtils.info(log, "获取证据照片信息...");
        LogUtils.record(log, "请求数据，systemId:{}，filter:{}", this.currentSystemId, queryText);
        CompletableFuture.supplyAsync(() -> photoFileService.findPhotoFiles(this.currentSystemId, queryText))
                .thenAccept(res -> Platform.runLater(() -> loadPhotoImage(res)))
                .exceptionally((ex) -> {
                    log.error("获取证据照片信息出错!", ex);
                    AppUtils.showMessageDialog("加载证据照片信息失败！",MessageType.FAIL,this.page);
                    return null;
                });
    }

    @FXML
    public void onPhotoFileDragOver(DragEvent event){
        event.acceptTransferModes(TransferMode.COPY);
    }

    @FXML
    public void onPhotoFileDragDropped(DragEvent event){
        Dragboard dragboard = event.getDragboard();
        if(!dragboard.hasFiles()){
            return;
        }
        this.appendSelectFiles(dragboard.getFiles());
    }

    private void appendSelectFiles(List<File> files) {
        var extensions = FileManage.getImageExtensions();
        var photoFiles = files.stream()
                .filter(f -> extensions.stream().anyMatch(e -> StringUtils.endsWithIgnoreCase(f.getName(), e)))
                .toList();

        var fileNames = photoFileService.findPhotoFileNames(this.currentSystemId);
        var existFileNames = photoFiles.stream()
                .map(File::getName)
                .filter(name -> fileNames.stream().anyMatch(fileName -> StringUtils.equals(name, fileName)))
                .collect(Collectors.toList());
        if (CollectionUtils.isNotEmpty(existFileNames)) {
            AlertDialog alertDialog = AlertDialog.builder()
                    .withMode(AlertMode.CONFIRM)
                    .withType(AlertType.WARN)
                    .withTitle("操作提示")
                    .withContent(String.format("当前系统已存在%s文件，是否继续进行？",String.join("、",existFileNames)))
                    .withContainer(this.page)
                    .build();
            alertDialog.show((confirm)->{
                if(!confirm){
                    return;
                }
                this.addPhotoImage(photoFiles,fileNames);
            });
        }else{
            this.addPhotoImage(photoFiles,fileNames);
        }
    }

    private void addPhotoImage(List<File> selectFiles,List<String> fileNames){
        var fileDir = FileManage.getPhotoFileDir(this.currentSystemId,currentLayerTarget.getName());
        setRenderEmpty(CollectionUtils.isEmpty(selectFiles));
        try {
            FileManage.createDirectory(FileUtils.getFile(fileDir));
            for (File file : selectFiles) {
                File distFile;
                if(compressSwitch.isSelected()){
                    distFile = compressFile(file,fileDir,fileNames);
                }else{
                    if (FileManage.existFileName(file.getName(), fileNames)) {
                        distFile = FileUtils.getFile(fileDir, FileManage.getFileRename(file.getName(), fileNames));
                    }else{
                        distFile = FileUtils.getFile(fileDir,file.getName());
                    }
                    FileManage.copyFile(file, distFile);
                }
                addPhotoImage(distFile);
            }
        }catch (IOException ex){
            log.error("【选择证据】添加证据照片文件出错：",ex);
            AppUtils.showMessageDialog("添加证据文件失败！",MessageType.FAIL,this.page);
        }
    }

    private File compressFile(File imgFile,String saveDir,List<String> fileNames) throws IOException{
        String newFileName = String.format("%s.jpg",FileManage.getFileNameWithoutSuffix(imgFile.getName()));
        File distFile;
        if (FileManage.existFileName(newFileName, fileNames)) {
            distFile = FileUtils.getFile(saveDir, FileManage.getFileRename(newFileName, fileNames));
        }else{
            distFile = FileUtils.getFile(saveDir,newFileName);
        }
        Thumbnails.of(imgFile)
                .scale(1f)
                .outputFormat("jpg")
                .outputQuality(AppConstants.IMAGE_COMPRESS)
                .toFile(distFile);
        return distFile;
    }

    private void initializeMenu(){
        menuListView.setCellFactory(listView -> new MenuListCell());
        menuListView.getSelectionModel()
                .selectedItemProperty()
                .addListener((observable,oldValue,newValue)-> {
                    if(Objects.nonNull(newValue)){
                        if(Objects.nonNull(timeline)){
                            timeline.stop();
                            timeline = null;
                        }
                        currentLayerTarget = LayerTarget.fromName(newValue.getTitle());
                        this.titleLbl.setText(String.format(TITLE_FORMAT,currentLayerTarget.getName()));
                        loadContent();
                    }
                });
        for(LayerTarget menu : LayerTarget.values()){
            menuListView.getItems().add(new MenuVO(menu.getValue(),"",menu.getName(),"folder-icon"));
        }
    }

    private void loadContent() {
        if (Objects.isNull(currentLayerTarget)) {
            return;
        }
        searchTxt.setText("");
        LogUtils.info(log, "[选择证据文件]获取证据照片信息...");
        LogUtils.record(log, "请求数据，systemId:{}，filter:{}", this.currentSystemId, currentLayerTarget);
        CompletableFuture.supplyAsync(() -> photoFileService.findPhotoFiles(this.currentSystemId, currentLayerTarget))
                .thenAccept(res -> Platform.runLater(() -> loadPhotoImage(res)))
                .exceptionally((ex) -> {
                    log.error("[选择证据文件]获取证据照片信息出错!", ex);
                    AppUtils.showMessageDialog("加载证据照片信息失败！",MessageType.FAIL,this.page);
                    return null;
                });
    }

    private void setRenderEmpty(boolean isEmpty){
        if(isEmpty && CollectionUtils.isEmpty(this.photoFlow.getChildren())){
            this.photoFlow.setVisible(false);
            this.emptyPane.setVisible(true);
            this.emptyLabel.setText("无证据文件");
        }else{
            this.photoFlow.setVisible(true);
            this.emptyPane.setVisible(false);
        }
    }

    private void loadPhotoImage(List<File> photoFiles){
        photoFlow.getChildren().clear();
        if(CollectionUtils.isEmpty(photoFiles)){
            setRenderEmpty(true);
            return;
        }else{
            setRenderEmpty(false);
        }
        //判断是否需要分块加载
        if(photoFiles.size() <= BLOCK_LIMIT_COUNT){
            photoFiles.forEach(this::addPhotoImage);
            return;
        }
        //加载第一块（前20个）内容
        photoFiles.stream().limit(BLOCK_LIMIT_COUNT).forEach(this::addPhotoImage);
        int count=photoFiles.size();
        int pageCount=count % BLOCK_LIMIT_COUNT > 0 ? count / BLOCK_LIMIT_COUNT + 1 : count / BLOCK_LIMIT_COUNT;
        //生成从第二块开始的延迟加载时间轴
        var keyFrames = IntStream.range(1,pageCount)
                .mapToObj(number-> new KeyFrame(Duration.millis(number*1000),(ae)->{
                    PageRequest pageRequest = PageRequest.of(number,BLOCK_LIMIT_COUNT);
                    photoFiles.stream()
                            .skip(pageRequest.getOffset())
                            .limit(BLOCK_LIMIT_COUNT)
                            .forEach(this::addPhotoImage);
                }))
                .toList();
        timeline = new Timeline();
        timeline.setCycleCount(1);
        timeline.setAutoReverse(false);
        timeline.getKeyFrames().clear();
        timeline.getKeyFrames().addAll(keyFrames);
        timeline.setOnFinished(ae->timeline = null);
        timeline.play();
    }

    private void addPhotoImage(File file){
        if(!file.exists()){
            return;
        }
        try {
            PhotoSelectControl photoSelectControl = new PhotoSelectControl(file,this.page);
            photoSelectControl.setOnRenameEvent(this::onRenameFileChick);
            photoSelectControl.setOnDeleteEvent(this::onDeleteFileClick);
            photoFlow.getChildren().add(photoSelectControl);
        }catch (IOException ex){
            log.error("加载证据照片文件失败：",ex);
            AppUtils.showMessageDialog("载入证据照片文件失败！",MessageType.FAIL,this.page);
        }
    }

    protected void onRenameFileChick(PhotoSelectControl source, String oldValue, String newValue){
        LogUtils.info(log, "[选择证据文件]执行证书照片文件重命名操作...");
        if(StringUtils.isBlank(newValue)){
            AppUtils.showMessageDialog("证据文件名不能为空！",MessageType.FAIL,this.page);
            return;
        }
        if(StringUtils.equals(oldValue,newValue)){
            source.setEditMode(false);
            return;
        }
        var newFileName = newValue.concat(FileManage.getFileSuffix(source.getPictureFile().getName()));
        var fileNames = photoFileService.findPhotoFileNames(currentSystemId);
        if(FileManage.existFileName(newFileName,fileNames)){
            AppUtils.showMessageDialog(String.format("当前系统已存在同名的%s文件！",newFileName),MessageType.FAIL,this.page);
        }else{
            if(!source.renamePhotoFile(newFileName)){
                source.renamePhotoFile(oldValue);
                AppUtils.showMessageDialog("证据照片文件重命名失败！",MessageType.FAIL,this.page);
                return;
            }
            String oldFileName = oldValue.concat(FileManage.getFileSuffix(source.getPictureFile().getName()));
            CompletableFuture.supplyAsync(()->photoFileService.refreshPhotoFileRename(currentSystemId,oldFileName,newFileName))
                    .thenAccept(res-> Platform.runLater(()-> {
                        if (res.isSuccess()) {
                            source.setEditMode(false);
                        } else {
                            source.renamePhotoFile(oldValue);
                            AppUtils.showMessageDialog(res.getMsg(),MessageType.FAIL,this.page);
                        }
                    }))
                    .exceptionally((ex) -> {
                        log.error("[选择证据文件]证书照片文件重命名出错!", ex);
                        return null;
                    });
        }
    }

    protected void onDeleteFileClick(PhotoSelectControl source){
        LogUtils.info(log, "【选择证据文件】执行证据照片删除操作...");
        AlertDialog alertDialog = AlertDialog.builder()
                .withMode(AlertMode.CONFIRM)
                .withType(AlertType.WARN)
                .withTitle("删除提示")
                .withContent("是否删除当前证据文件？")
                .withContainer(this.page)
                .build();
        alertDialog.show((confirm)->{
            if(!confirm){
                return;
            }
            checkDeleteFile(source);
        });
    }

    protected void checkDeleteFile(PhotoSelectControl source) {
        String fileName = source.getPictureFile().getName();
        LogUtils.info(log, "【选择证据文件】删除证据文件前的检查...");
        LogUtils.record(log, "请求数据，systemId:{}，data:{}", currentSystemId, fileName);
        CompletableFuture.supplyAsync(() -> photoFileService.checkPhotoFileDelete(currentSystemId, fileName))
                .thenAccept(res -> Platform.runLater(() -> {
                    if (res.isSuccess()) {
                        deletePhotoFile(source);
                    } else {
                        AlertDialog alertDialog = AlertDialog.builder()
                                .withMode(AlertMode.CONFIRM)
                                .withType(AlertType.WARN)
                                .withTitle("删除警告")
                                .withContent(res.getMsg())
                                .withContainer(this.page)
                                .build();
                        alertDialog.show((confirm) -> {
                            if (confirm) {
                                deletePhotoFile(source);
                            }
                        });
                    }
                }))
                .exceptionally((ex) -> {
                    log.error("【选择证据文件】删除证据文件前的检查出错!", ex);
                    return null;
                });
    }

    protected void deletePhotoFile(PhotoSelectControl source) {
        String fileName = source.getPictureFile().getName();
        LogUtils.info(log, "【选择证据文件】删除证据文件...");
        LogUtils.record(log, "请求数据，systemId:{}，data:{}", currentSystemId, fileName);
        CompletableFuture.supplyAsync(() -> photoFileService.deletePhotoFileRef(currentSystemId, fileName))
                .thenAccept(res -> Platform.runLater(() -> {
                    if (res.isSuccess()) {
                        this.photoFlow.getChildren().remove(source);
                        removeStoreFile(source.getPictureFile());
                        if (CollectionUtils.isEmpty(this.photoFlow.getChildren())) {
                            setRenderEmpty(true);
                        }
                    } else {
                        MessageDialog.builder()
                                .withContainer(this.page)
                                .withType(MessageType.FAIL).withMessage(res.getMsg())
                                .build().show();
                    }
                }))
                .exceptionally((ex) -> {
                    log.error("【选择证据文件】删除证据文件出错!", ex);
                    return null;
                });
    }

    protected void removeStoreFile(@NonNull File file){
        try {
            FileUtils.delete(file);
        }catch (IOException ex){
            log.error(String.format("【选择证据文件】删除%s证据文件失败：",file.getName()),ex);
            MessageDialog.builder()
                    .withContainer(this.page)
                    .withType(MessageType.FAIL).withMessage("移除证据文件失败")
                    .build().show();
        }
    }
}